# Python module properties
__author__ = "Stefan Blanke (greenarrow) (greenarrow@users.sourceforge.net)"
__credits__ = ""
__license__ = "GPL 3.0"
__version__ = "0.4"
__licence__ = """
RSDL is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

RSDL is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with RSDL.  If not, see <http://www.gnu.org/licenses/>.
"""


import os, threading, sys, time, mechanize, exceptions, Queue
import xml.dom.minidom
from xml.dom.minidom import Node

debug = True


# Find user directory
if sys.platform=="win32":
	homePath = os.path.join( os.environ.get("USERPROFILE"), "Application Data" )
else:
	homePath = os.environ.get("HOME")

cookiePath = os.path.join(homePath, '.rsdl/cookies.txt')


class _rsdlError(exceptions.Exception):
	"""rsdl Error"""
	def __init__(self, msg):
		self.msg = msg
		return
	
	def __str__(self):
		return "An RSDL Error has occured: " + str(self.msg)


def _fingerprintText (searchText, params):
	"""Search a block of text for two strings and return the text between them (with offsets)"""
	startString, startOffset, endString, endOffset = params
	remainingString = searchText
	returnList = []
	while len (remainingString) > 0:
		startResult, endResult = remainingString.find (startString), remainingString.find (endString)
		if startResult <= 0 or endResult <= 0 : break
		startPos =  startResult + len (startString) + startOffset
		endPos =  + endResult + endOffset
		foundText = remainingString [startPos : endPos]
		returnList.append (foundText)
		remainingString = remainingString [endPos + len (endString) : len (remainingString)]
	return returnList


def downloadCookie(username, password):
	"""Downloading authentication cookie"""
	
	# download rapidshare cookie
	tempPath = os.path.join(homePath, '.rsdl/tempcookie')
	#cookieCommand = 'wget --no-check-certificate -O "' + tempPath + '" --save-cookies "' + cookiePath + '" --post-data="login=' + str(username) + '&password=' + str(password) + '" https://ssl.rapidshare.com/cgi-bin/premiumzone.cgi'
	cookieCommand = 'wget --quiet --no-verbose --no-check-certificate -O "' + tempPath + '" --save-cookies "' + cookiePath + '" --post-data="login=' + str(username) + '&password=' + str(password) + '" https://ssl.rapidshare.com/cgi-bin/premiumzone.cgi'
	#print "cc [" + cookieCommand + "]"
	os.system(cookieCommand)
	os.remove(tempPath)
	
	# make sure we got the file
	if not os.path.exists(cookiePath):
		raise _rsdlError("Failed to download cookie")
	f = open(cookiePath, 'r')
	lines = f.readlines()
	f.close()
	
	# make sure it contains the cookie data
	if len(lines) < 5:
		os.remove(cookiePath)
		raise _rsdlError("Failed to download cookie")
	
	# Replace first three lines (mechanize does not accept without correct header)
	lines[0] = "# Netscape HTTP Cookie File"
	lines[1] = "# http://www.netscape.com/newsref/std/cookie_spec.html"
	lines[2] = "# This is a generated file! Do not edit."
	newText = ""
	for l in lines:
		newText += l + '\n'
	f = open(cookiePath, 'w')
	f.write(newText)
	f.close

def checkCookie():
	return os.path.exists(cookiePath)

def deleteCookie():
	"""Delete authentication cookie"""
	os.remove(cookiePath)


class Task:
	"""Class for task (file to download)"""
	def __init__(self, url, directLink, speed, destinationPath, parent = None, group = None, size = None, status = "Idle", password = None, extract = False):
		self.parent = parent
		self.url = url
		# TODO - add some validity checking on url
		self.directLink = directLink
		self.destinationPath = destinationPath
		self.speed = speed
		self.group = group
		if size == None:
			self.getSize()
			# do retries here? TODO
			# theading?
		else:
			self.size = size
		path, self.localName = os.path.split(self.url)
		self.localFile = os.path.join(self.destinationPath, self.localName)
		self.progress = 0
		self.status = status
		self.currentSpeed = "-"
		self.timeRemaining = "-"
	
	# Get rapidshare mirrors
	def getMirrors(self):
		self.status = "Finding mirrors"
		debug = False
		
		br = mechanize.Browser()
		
		# Load authentication cookie from file
		cookies = mechanize.MozillaCookieJar()
		cookies.load(cookiePath)
		br.set_cookiejar(cookies)
		
		if self.url[ : 7 ].lower() != "http://":
			self.url = "http://" + self.url
		
		try:
			br.open( self.url )
		except:
			raise _rsdlError("Failed to open link, server may be down")
		else:
			#print "Link good"
			pass
		try:
			# Update changed this to one as order of free and premium forms switched
			br.select_form(nr=1)
		except:
			raise _rsdlError("Failed to select form, rsdl may need updating")
		try:
			r = br.submit()
		except:
			raise _rsdlError("Failed to submit form, may be server issue")
		
		if debug:
			f = open("debug1.htm", "w")
			f.write( r.get_data() )
			f.close()
		
		# Process mirror html file, extract list of mirrors
		mirrorTxt = r.get_data()
		mirrorBlockParam = "Wants to download the following file:", 0, "img2/download_file.jpg", 0
		mirrorBlock = _fingerprintText( mirrorTxt, mirrorBlockParam )
		mirrorParam = "document.dlf.action", 2, "<br />", -3
		mirrorsGrab = _fingerprintText( mirrorBlock[0], mirrorParam )
		self.mirrors = []
		for m in mirrorsGrab:
			cutPos = m.find(';" /> ') - 1
			if len( m[ : cutPos ] ) > 0:
				self.mirrors.append( m[ : cutPos ] )
		if debug:
			print self.mirrors
	
	# Downloading header for file size (in bytes)
	def getSize(self):
		self.status = "Getting file size"
		if not os.path.exists( cookiePath ):
			downloadCookie(self.parent.username, self.parent.password)
		headerPath = os.path.join(homePath, '.rsdl/tempheader')
		sizeCommand = 'wget --load-cookies "' + cookiePath + '" -o "' + headerPath + '" --spider ' + self.url
		os.system(sizeCommand)
		f = open(headerPath, 'r')
		lines = f.readlines()
		f.close()
		os.remove(headerPath)
		self.size = None
		# Extract file size from header file
		for l in lines:
			if l[ :8 ] == 'Length: ':
				sizeText = l[ 8: l[ 9: ].find(' ') + 9 ]
				sizeText = sizeText.replace(',', '')
				self.size = int(sizeText)
		if self.size == None:
			raise _rsdlError("Filesize reading failed")
		#print "File size =", self.size, 'bytes'
	
	# Download file
	def download(self):
		if not self.directLink:
			self.getMirrors()
			# need to put try in hear to brin g back reties
			#	print "Mirrors failed, trying again"
			#	time.sleep(10)
			self.url = self.mirrors[0]
			self.directLink = True
		self.status = "Downloading"
		# Downloading file
		speedString = ""
		if self.speed != None:
			speedString = '--limit-rate=' + str( int(self.speed * 1000) ) + ' '
		dlCommand = 'wget --quiet --no-verbose --load-cookies "' + cookiePath + '" ' + speedString + '-O "' + str( os.path.join(self.destinationPath, self.localName) ) + '" ' + str(self.url)
		#print "run" + "'" + dlCommand + "'"
		os.system(dlCommand)


class TaskList:
	"""Class for task list (list of files to download)"""
	#def __init__(self, username, password, filename = None):
	def __init__(self, filename = None):
		"""Create task list with username and password"""
		if filename == None:
			self.filename = os.path.join(homePath, ".rsdl/tasks.xml")
		else:
			self.filename = filename
		
		#self.username = username
		#self.password = password
		self.tasks = []
		self.downloader = None
	
	def load(self):
		"""Load tasklist from xml file"""
		if os.path.exists( self.filename ):
			doc = xml.dom.minidom.parse( self.filename )
			self.tasks = []
			for rsdlNode in doc.getElementsByTagName("RSDL"):
				version = rsdlNode.getAttribute("version")
				for taskNode in rsdlNode.getElementsByTagName("TASK"):
					url = str( taskNode.getAttribute("url") )
					if taskNode.getAttribute("directLink") == "True":
						directLink = True
					else:
						directLink = False
	
					if taskNode.getAttribute("speed") == "None":
						speed = None
					else:
						speed = float( taskNode.getAttribute("speed") )
					
					destinationPath = str( taskNode.getAttribute("destinationPath") )
					group = str( taskNode.getAttribute("group") )
					size = int( taskNode.getAttribute("size") )
					status = str( taskNode.getAttribute("status") )
				
					newTask = Task(url, directLink, speed, destinationPath, self, group, size, status)
					self.tasks.append(newTask)
		else:
			if not os.path.exists( os.path.join(homePath, ".rsdl") ):
				os.mkdir( os.path.exists( os.path.join(homePath, ".rsdl") ) )
			self.save()
	
	def save(self):
		"""Save tasklist to xml file"""
		version = 0.1
		text = '<RSDL version="' + str(version) + '">\n'
		for task in self.tasks:
			text += '\t<TASK url="' + str(task.url) + '" directLink="' + str(task.directLink) + '" speed="' + str(task.speed) + '" destinationPath="' + str(task.destinationPath) + '" group="' + str(task.group) + '" size="' + str(task.size) + '" status="' + str(task.status) + '"/>\n'
		text += '</RSDL>'
		f = open( self.filename, "w" )
		f.write(text)
		f.close()
	
	def addTask(self, task):
		"""Add a new task"""
		task.parent = self
		self.tasks.append(task)
	
	def deleteTasks(self, tasks):
		"""Delete a list of tasks"""
		for task in tasks:
			self.tasks.remove(task)
	
	def deleteComplete(self):
		"""Delete all completed tasks"""
		toDelete = []
		for task in self.tasks:
			if task.status == "Complete":
				toDelete.append(task)
		self.deleteTasks(toDelete)
	
	def moveTasksUp(self, tasks):
		"""Move a task up in list"""
		moveSafe = True
		for task in tasks:
			if self.tasks.index(task) <= 0:
				moveSafe = False
		if moveSafe:
			for task in tasks:
				index = self.tasks.index(task)
				self.tasks[index], self.tasks[index - 1] = self.tasks[index - 1], self.tasks[index]
	
	def moveTasksDown(self, tasks):
		"""Move a task down in list"""
		moveSafe = True
		for task in tasks:
			if self.tasks.index(task) + 1 >= len(self.tasks):
				moveSafe = False
		if moveSafe:
			for task in tasks:
				index = self.tasks.index(task)
				self.tasks[index], self.tasks[index + 1] = self.tasks[index + 1], self.tasks[index]
	
	def getTasks(self, indecies):
		"""Get list of tasks from list of indecies"""
		tasks = []
		for i in indecies:
			tasks.append(self.tasks[i])
		return tasks
	
	def downloadTasks(self, threads, tasks=None):
		"""Start downloading selected tasks"""
		# If no task arg use all in own list
		if tasks == None:
			tasks = self.tasks
		
		for task in tasks:
			task.status = "Queued"

		self.feedback.listUpdate()
		self.save()

		#print len(selected), self.taskList.downloading
		if len(tasks):
			# If downloader does not exist then start it and add queued tasks
			if self.downloader == None:
				self.downloader = _Downloader(self.feedback, self, threads)
				for task in tasks:
					self.downloader.addTask(task)
				self.downloader.start()
			# If downloader already exists then just add tasks to queue
			else:
				for task in tasks:
					self.downloader.addTask(task)


class _Downloader(threading.Thread):
	"""Main download manger thread, controls individual download threads"""
	def __init__(self, feedback, tasklist, numThreads):
		threading.Thread.__init__(self)
		self.tasklist = tasklist
		self.numThreads = numThreads
		self.feedback = feedback
		self.queue = Queue.Queue(0)
		#feedback.setStatus("this is a test")
	
	def addTask(self, task):
		self.queue.put(task)
	
	def run(self):
		print "dlr"
		threads = []
		
		for i in range(self.numThreads):
			print "made thrd"
			newThread = _DownloadThread(self.feedback, self.queue)
			print "start thrd"
			newThread.start()
			threads.append( newThread )
		
		self.queue.join()
		
		for t in threads:
			t.join()
		
		# do proper feedback complete here? this may not be needed as task.completed variable is changed and everything is done in variables
		# put wait for teminate events here and in main?
		
		# If there are no tasks to download then finish thread
		self.tasklist.downloader = None
		print "dll done"


class _DownloadThread(threading.Thread):
	"""Class to download a task"""
	def __init__(self, feedback, queue):
		threading.Thread.__init__(self)
		self.feedback = feedback
		self.queue = queue
		self.alive = True
	
	def run(self):
		while self.alive:
			# Get next task from queue
			print "wt" # threads are blocked here and so can not be terminated TODO fix
			task = self.queue.get()
			print "gt"
			# Check we still want to download it
			print task.status
			if task.status == "Queued":
				# Create and start download monitor
				self.dlMonitor = _DownloadMonitor(task, self.feedback)
				self.dlMonitor.start()
				# Download the task
				task.download()
				# Terminate monitor and wait for it to finish
				self.dlMonitor.terminate()
				self.dlMonitor.endEvent.wait()
				# Set task attributes as complete
				task.complete = True
				task.progress = 100
				task.status = "Complete"
				# Update gui & tell download manager we are done
				self.feedback.listUpdate()
			self.queue.task_done()
		print "dtd"
	
	def terminate(self):
		self.alive = False


class _DownloadMonitor(threading.Thread):
	"""Class to monitor the progress of a download. Created by individual _DownloadThread()
	Watches the file and compares size file size obtained from http headers to get progress."""
	def __init__(self, task, feedback):
		threading.Thread.__init__(self)
		self.task = task
		self.feedback = feedback
		self.alive = True
		self.task.progress = 0
		self.endEvent = threading.Event()
	
	def run(self):
		timePeriod = 1
		lastSize = 0
		lastSpeed = 0
		while self.alive:
			if os.path.exists(self.task.localFile):
				localSize = os.stat(self.task.localFile)[6]
				deltaSize = localSize - lastSize
				# Calculate progress as %
				self.task.progress = int( float( localSize ) / float(self.task.size) * 100 )
				# Calculate download speed in KB/s, average with last value
				newSpeed = (deltaSize / timePeriod / 1024)
				currentSpeed = ( newSpeed + lastSpeed ) / 2
				
				# If we are actually downloading:
				if currentSpeed > 0:
					self.task.currentSpeed = str( currentSpeed ) + " KB/s"
					# Calculate the seconds remaining for download
					seconds = ( ( float(self.task.size - int(localSize) ) / 1024 ) / float(currentSpeed) )
					# Produce formatted time remaining string
					if seconds < 60:
						self.task.timeRemaining = str( int(seconds) ) + " sec"
					elif seconds < 3600:
						self.task.timeRemaining = str( int(seconds / 60) ) + " min " + str( int(seconds) - ( int(seconds / 60) * 60 ) ) + " sec"
					else:
						self.task.timeRemaining = str( int(seconds / 3600) ) + " hr " + str( int(seconds / 60) - ( int(seconds / 3600) + 3600 ) ) + " min"
				else:
					# we are not downloading
					self.task.currentSpeed = "-"
					self.task.timeRemaining = "-"
				
				lastSpeed = newSpeed
				lastSize = localSize
				
			self.feedback.listUpdate()
			time.sleep(timePeriod)
		
		# Monitor finished, set blank values
		self.task.currentSpeed = "-"
		self.task.timeRemaining = "-"
		# Tell download thread we are done
		self.endEvent.set()
		self.endEvent.clear()
		print "dmd"
	
	def terminate(self):
		self.alive = False








